"""
Framework Detection Engine

Analyzes configuration files to determine which extension should handle them
using pattern matching, content analysis, and heuristics.
"""

import json
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple, Set
from dataclasses import dataclass
from enum import Enum
import re
import logging

import yaml

logger = logging.getLogger(__name__)


class ConfidenceLevel(Enum):
    """Confidence levels for framework detection."""
    VERY_HIGH = "very_high"  # 90-100%
    HIGH = "high"           # 70-89%
    MEDIUM = "medium"       # 50-69%
    LOW = "low"            # 20-49%
    VERY_LOW = "very_low"   # 0-19%


@dataclass
class DetectionPattern:
    """A pattern for detecting framework compatibility."""
    name: str
    pattern_type: str  # 'xpath', 'yaml_path', 'regex', 'filename', 'content'
    pattern: str
    expected_values: Optional[List[str]] = None
    weight: float = 1.0
    required: bool = False
    
    
@dataclass 
class DetectionRule:
    """A rule combining multiple patterns for framework detection."""
    framework_name: str
    patterns: List[DetectionPattern]
    minimum_confidence: float = 0.5
    
    
@dataclass
class DetectionResult:
    """Result of framework detection."""
    framework_name: str
    confidence: float
    confidence_level: ConfidenceLevel
    matched_patterns: List[str]
    details: Dict[str, Any]


class FrameworkDetector:
    """
    Engine for detecting which framework/extension should handle a configuration file.
    
    Uses multiple detection strategies:
    - File extension and naming patterns
    - XML XPath expressions
    - YAML path expressions
    - Content-based regex patterns
    - Heuristic analysis
    """
    
    def __init__(self):
        """Initialize the framework detector."""
        self.detection_rules: Dict[str, DetectionRule] = {}
        self._load_default_rules()
        
    def register_detection_rule(self, rule: DetectionRule) -> None:
        """Register a new detection rule for a framework."""
        self.detection_rules[rule.framework_name] = rule
        logger.debug(f"Registered detection rule for framework: {rule.framework_name}")
        
    def detect_framework(self, config_path: Path) -> List[DetectionResult]:
        """
        Detect which frameworks can handle the given configuration file.
        
        Returns results sorted by confidence (highest first).
        """
        if not config_path.exists():
            logger.warning(f"Configuration file does not exist: {config_path}")
            return []
            
        results = []
        
        # Analyze the configuration file
        file_info = self._analyze_config_file(config_path)
        if not file_info:
            return []
            
        # Test each detection rule
        for framework_name, rule in self.detection_rules.items():
            result = self._test_detection_rule(rule, file_info, config_path)
            if result and result.confidence >= rule.minimum_confidence:
                results.append(result)
                
        # Sort by confidence (highest first)
        results.sort(key=lambda x: x.confidence, reverse=True)
        
        logger.info(f"Detected {len(results)} compatible frameworks for {config_path.name}")
        return results
        
    def find_best_framework(self, config_path: Path) -> Optional[DetectionResult]:
        """Find the best framework match for a configuration file."""
        results = self.detect_framework(config_path)
        return results[0] if results else None
        
    def _analyze_config_file(self, config_path: Path) -> Optional[Dict[str, Any]]:
        """Analyze a configuration file and extract relevant information."""
        file_info = {
            'path': config_path,
            'filename': config_path.name,
            'extension': config_path.suffix.lower(),
            'size': config_path.stat().st_size,
            'content': None,
            'parsed_content': None,
            'file_type': None
        }
        
        try:
            # Read file content
            with open(config_path, 'r', encoding='utf-8') as f:
                content = f.read()
            file_info['content'] = content
            
            # Determine file type and parse if possible
            if file_info['extension'] in ['.xml']:
                file_info['file_type'] = 'xml'
                try:
                    file_info['parsed_content'] = ET.fromstring(content)
                except ET.ParseError as e:
                    logger.warning(f"Failed to parse XML file {config_path}: {e}")
                    
            elif file_info['extension'] in ['.yaml', '.yml']:
                file_info['file_type'] = 'yaml'
                try:
                    file_info['parsed_content'] = yaml.safe_load(content)
                except yaml.YAMLError as e:
                    logger.warning(f"Failed to parse YAML file {config_path}: {e}")
                    
            elif file_info['extension'] in ['.json']:
                file_info['file_type'] = 'json'
                try:
                    file_info['parsed_content'] = json.loads(content)
                except json.JSONDecodeError as e:
                    logger.warning(f"Failed to parse JSON file {config_path}: {e}")
                    
            else:
                # Try to detect file type from content
                if content.strip().startswith('<?xml') or content.strip().startswith('<'):
                    file_info['file_type'] = 'xml'
                    try:
                        file_info['parsed_content'] = ET.fromstring(content)
                    except ET.ParseError:
                        pass
                elif content.strip().startswith('{') or content.strip().startswith('['):
                    file_info['file_type'] = 'json'
                    try:
                        file_info['parsed_content'] = json.loads(content)
                    except json.JSONDecodeError:
                        pass
                else:
                    # Try YAML
                    try:
                        parsed = yaml.safe_load(content)
                        if isinstance(parsed, (dict, list)):
                            file_info['file_type'] = 'yaml'
                            file_info['parsed_content'] = parsed
                    except yaml.YAMLError:
                        pass
                        
            return file_info
            
        except Exception as e:
            logger.error(f"Failed to analyze config file {config_path}: {e}")
            return None
            
    def _test_detection_rule(self, rule: DetectionRule, file_info: Dict[str, Any], config_path: Path) -> Optional[DetectionResult]:
        """Test a detection rule against file information."""
        matched_patterns = []
        total_weight = sum(p.weight for p in rule.patterns)
        matched_weight = 0.0
        required_matches = 0
        required_patterns = sum(1 for p in rule.patterns if p.required)
        details = {}
        
        for pattern in rule.patterns:
            match_result = self._test_pattern(pattern, file_info, config_path)
            if match_result:
                matched_patterns.append(pattern.name)
                matched_weight += pattern.weight
                if pattern.required:
                    required_matches += 1
                details[pattern.name] = match_result
                
        # Check if all required patterns matched
        if required_patterns > 0 and required_matches < required_patterns:
            return None
            
        # Calculate confidence
        base_confidence = matched_weight / total_weight if total_weight > 0 else 0.0
        
        # Apply bonuses/penalties
        confidence = self._adjust_confidence(base_confidence, matched_patterns, file_info, rule)
        
        # Determine confidence level
        confidence_level = self._get_confidence_level(confidence)
        
        return DetectionResult(
            framework_name=rule.framework_name,
            confidence=confidence,
            confidence_level=confidence_level,
            matched_patterns=matched_patterns,
            details=details
        )
        
    def _test_pattern(self, pattern: DetectionPattern, file_info: Dict[str, Any], config_path: Path) -> Optional[Any]:
        """Test a single detection pattern."""
        try:
            if pattern.pattern_type == 'filename':
                return self._test_filename_pattern(pattern, file_info)
            elif pattern.pattern_type == 'xpath':
                return self._test_xpath_pattern(pattern, file_info)
            elif pattern.pattern_type == 'yaml_path':
                return self._test_yaml_path_pattern(pattern, file_info)
            elif pattern.pattern_type == 'regex':
                return self._test_regex_pattern(pattern, file_info)
            elif pattern.pattern_type == 'content':
                return self._test_content_pattern(pattern, file_info)
            else:
                logger.warning(f"Unknown pattern type: {pattern.pattern_type}")
                return None
                
        except Exception as e:
            logger.debug(f"Pattern test failed for {pattern.name}: {e}")
            return None
            
    def _test_filename_pattern(self, pattern: DetectionPattern, file_info: Dict[str, Any]) -> Optional[str]:
        """Test filename-based pattern."""
        import fnmatch
        filename = file_info['filename']
        
        if fnmatch.fnmatch(filename, pattern.pattern):
            return filename
        return None
        
    def _test_xpath_pattern(self, pattern: DetectionPattern, file_info: Dict[str, Any]) -> Optional[Any]:
        """Test XPath-based pattern."""
        if file_info['file_type'] != 'xml' or file_info['parsed_content'] is None:
            return None
            
        root = file_info['parsed_content']
        elements = root.findall(pattern.pattern)
        
        if not elements:
            return None
            
        # Extract values
        values = []
        for elem in elements:
            if elem.text:
                values.append(elem.text.strip())
            # Also check attributes
            for attr_value in elem.attrib.values():
                values.append(attr_value.strip())
                
        # Check against expected values if specified
        if pattern.expected_values:
            matches = [v for v in values if v in pattern.expected_values]
            return matches if matches else None
        else:
            return values if values else None
            
    def _test_yaml_path_pattern(self, pattern: DetectionPattern, file_info: Dict[str, Any]) -> Optional[Any]:
        """Test YAML path-based pattern."""
        if file_info['file_type'] not in ['yaml', 'json'] or file_info['parsed_content'] is None:
            return None
            
        data = file_info['parsed_content']
        
        # Navigate the path (e.g., "framework.type" -> ["framework", "type"])
        path_parts = pattern.pattern.split('.')
        current = data
        
        try:
            for part in path_parts:
                if isinstance(current, dict):
                    current = current.get(part)
                elif isinstance(current, list) and part.isdigit():
                    current = current[int(part)]
                else:
                    return None
                    
                if current is None:
                    return None
                    
            # Check against expected values if specified
            if pattern.expected_values:
                if str(current) in pattern.expected_values:
                    return current
                else:
                    return None
            else:
                return current
                
        except (KeyError, IndexError, ValueError):
            return None
            
    def _test_regex_pattern(self, pattern: DetectionPattern, file_info: Dict[str, Any]) -> Optional[List[str]]:
        """Test regex-based pattern."""
        content = file_info['content']
        if not content:
            return None
            
        matches = re.findall(pattern.pattern, content, re.IGNORECASE | re.MULTILINE)
        
        if not matches:
            return None
            
        # Check against expected values if specified
        if pattern.expected_values:
            valid_matches = [m for m in matches if m in pattern.expected_values]
            return valid_matches if valid_matches else None
        else:
            return matches
            
    def _test_content_pattern(self, pattern: DetectionPattern, file_info: Dict[str, Any]) -> Optional[bool]:
        """Test content-based pattern (simple string presence)."""
        content = file_info['content']
        if not content:
            return None
            
        if pattern.pattern.lower() in content.lower():
            return True
        return None
        
    def _adjust_confidence(self, base_confidence: float, matched_patterns: List[str], 
                         file_info: Dict[str, Any], rule: DetectionRule) -> float:
        """Apply confidence adjustments based on additional heuristics."""
        confidence = base_confidence
        
        # Bonus for matching multiple patterns
        if len(matched_patterns) > 1:
            confidence += 0.1 * (len(matched_patterns) - 1)
            
        # Bonus for specific file types
        if file_info['file_type'] == 'xml' and any('xml' in pattern.lower() for pattern in matched_patterns):
            confidence += 0.05
            
        # Penalty for very small files (likely incomplete)
        if file_info['size'] < 100:
            confidence -= 0.1
            
        # Ensure confidence stays in valid range
        return max(0.0, min(1.0, confidence))
        
    def _get_confidence_level(self, confidence: float) -> ConfidenceLevel:
        """Convert numeric confidence to confidence level."""
        if confidence >= 0.9:
            return ConfidenceLevel.VERY_HIGH
        elif confidence >= 0.7:
            return ConfidenceLevel.HIGH
        elif confidence >= 0.5:
            return ConfidenceLevel.MEDIUM
        elif confidence >= 0.2:
            return ConfidenceLevel.LOW
        else:
            return ConfidenceLevel.VERY_LOW
            
    def _load_default_rules(self) -> None:
        """Load default detection rules for common frameworks."""
        
        # Original GCR-NCCL framework
        original_gcr_rule = DetectionRule(
            framework_name="original-gcr-nccl",
            patterns=[
                DetectionPattern(
                    name="gcr_solver_type",
                    pattern_type="xpath", 
                    pattern=".//Parameter[@name='solver_type']",
                    expected_values=["gcr", "ca-gcr", "gmres", "bca-gmres"],
                    weight=3.0
                ),
                DetectionPattern(
                    name="nccl_backend",
                    pattern_type="xpath",
                    pattern=".//Parameter[@name='gpu_backend']", 
                    expected_values=["cuda", "nccl"],
                    weight=2.0
                ),
                DetectionPattern(
                    name="gcr_config_filename",
                    pattern_type="filename",
                    pattern="*gcr*",
                    weight=1.0
                ),
                DetectionPattern(
                    name="framework_indicator", 
                    pattern_type="xpath",
                    pattern=".//Parameter[@name='framework']",
                    expected_values=["original", "gcr-nccl"],
                    weight=2.0
                )
            ],
            minimum_confidence=0.4
        )
        
        # PETSc-only framework
        petsc_only_rule = DetectionRule(
            framework_name="petsc-only",
            patterns=[
                DetectionPattern(
                    name="petsc_solver_type",
                    pattern_type="xpath",
                    pattern=".//Parameter[@name='solver_type']", 
                    expected_values=["gcr", "gmres", "cg", "bicg"],
                    weight=2.0
                ),
                DetectionPattern(
                    name="petsc_framework",
                    pattern_type="xpath",
                    pattern=".//Parameter[@name='framework']",
                    expected_values=["petsc", "petsc-only"],
                    weight=3.0
                ),
                DetectionPattern(
                    name="petsc_config_filename",
                    pattern_type="filename", 
                    pattern="*petsc*",
                    weight=1.0
                ),
                DetectionPattern(
                    name="no_nccl_backend",
                    pattern_type="xpath",
                    pattern=".//Parameter[@name='gpu_backend']",
                    expected_values=["cpu"],
                    weight=1.0
                )
            ],
            minimum_confidence=0.4
        )
        
        # Hybrid PETSc-Kokkos framework
        hybrid_rule = DetectionRule(
            framework_name="hybrid-petsc-kokkos", 
            patterns=[
                DetectionPattern(
                    name="hybrid_framework",
                    pattern_type="xpath",
                    pattern=".//Parameter[@name='framework']",
                    expected_values=["hybrid", "petsc-kokkos"],
                    weight=3.0,
                    required=True
                ),
                DetectionPattern(
                    name="kokkos_backend",
                    pattern_type="xpath",
                    pattern=".//Parameter[@name='kokkos_backend']",
                    weight=2.0
                ),
                DetectionPattern(
                    name="hybrid_config_filename",
                    pattern_type="filename",
                    pattern="*hybrid*",
                    weight=1.0
                )
            ],
            minimum_confidence=0.5
        )
        
        # Register default rules
        self.register_detection_rule(original_gcr_rule)
        self.register_detection_rule(petsc_only_rule)
        self.register_detection_rule(hybrid_rule)
        
        # YAML-based detection rules
        yaml_gcr_rule = DetectionRule(
            framework_name="original-gcr-nccl",
            patterns=[
                DetectionPattern(
                    name="yaml_framework_type",
                    pattern_type="yaml_path",
                    pattern="framework.type",
                    expected_values=["gcr-nccl", "original"],
                    weight=3.0
                ),
                DetectionPattern(
                    name="yaml_solver_type", 
                    pattern_type="yaml_path",
                    pattern="solver.type",
                    expected_values=["gcr", "ca-gcr", "gmres"],
                    weight=2.0
                )
            ],
            minimum_confidence=0.4
        )
        
        self.register_detection_rule(yaml_gcr_rule)
        
        logger.info(f"Loaded {len(self.detection_rules)} default detection rules")
        
    def get_supported_frameworks(self) -> List[str]:
        """Get list of frameworks that have detection rules."""
        return list(self.detection_rules.keys())
        
    def get_detection_rule(self, framework_name: str) -> Optional[DetectionRule]:
        """Get detection rule for a specific framework."""
        return self.detection_rules.get(framework_name)
        
    def remove_detection_rule(self, framework_name: str) -> bool:
        """Remove detection rule for a framework."""
        if framework_name in self.detection_rules:
            del self.detection_rules[framework_name]
            logger.info(f"Removed detection rule for framework: {framework_name}")
            return True
        return False